package libs.game.sokoban;

import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import libs.game.AbstractSolver;
import libs.game.Coordinates;
import libs.game.GamePlan;


/**
 *
 * @author papi
 */
public class SokobanSolver extends AbstractSolver {

    Set<Coordinates> fixedBoxes = new HashSet<Coordinates>();

    Set<Coordinates> targets = new HashSet<Coordinates>();
    
    public static final int SEARCH_LIMIT = 1000;

    @Override
    public Comparator<GamePlan> getComparator() {
        return new Comparator<GamePlan>() {

            @Override
            public int compare(GamePlan a, GamePlan b) {
                // It counts target fields where a box is.
                int numA = 0;
                for (Coordinates target : targets) {
                    if ((a.getField(target) == SokobanGamePlan.FIELD_BOX_ON_EMPTY) || (a.getField(target) == SokobanGamePlan.FIELD_BOX_ON_GOAL)) {
                        numA++;
                    }
                }
                // It counts target fields where a box is.
                int numB = 0;
                for (Coordinates target : targets) {
                    if ((b.getField(target) == SokobanGamePlan.FIELD_BOX_ON_EMPTY) || (b.getField(target) == SokobanGamePlan.FIELD_BOX_ON_GOAL)) {
                        numB++;
                    }
                }
                return numB - numA;
            }
        };
    }

    @Override
    protected boolean isLocked(GamePlan plan) {
        if (!(plan instanceof SokobanGamePlan)) {
            throw new IllegalArgumentException("Argument 'plan' is not instance of SokobanGamePlan.");
        }
        return new SokobanSimpleDeadlock((SokobanGamePlan)plan).isLocked();
    }

    @Override
    public int limit() {
        return SEARCH_LIMIT;
    }

    @Override
    protected void processSituations(GamePlan situation1, GamePlan situation2) {
        if (situation1 == null) {
            throw new NullPointerException("Argument 'situation1' is null");
        }
        if (situation2 == null) {
            throw new NullPointerException("Argument 'situation2' is null");
        }
        // Checking if the situatioans are sokoban plans and the have the same size.
        if (!(situation1 instanceof SokobanGamePlan)) {
            throw new IllegalArgumentException("The original situation is not sokoban game plan");
        }
        if (!(situation2 instanceof SokobanGamePlan)) {
            throw new IllegalArgumentException("The target  situation is not sokoban game plan");
        }
        if (situation1.rows() != situation2.rows() || situation1.columns() != situation2.columns()) {
            throw new IllegalArgumentException("The situations has different size.");
        }
        // Looking for difference (the most important are the moved boxes).
        Set<Coordinates> boxes = new HashSet<Coordinates>();
        for (int row = 0; row < situation1.rows(); row++) {
            for (int column = 0; column < situation1.columns(); column++) {
                Coordinates currentCoords = new Coordinates(column, row);
                switch (situation1.getField(currentCoords)) {
                    case SokobanGamePlan.FIELD_BOX_ON_EMPTY:
                        // The box on this field has been moved.
                        if ((situation2.getField(currentCoords) == SokobanGamePlan.FIELD_EMPTY) ||
                                (situation2.getField(currentCoords) == SokobanGamePlan.FIELD_PERSON_ON_EMPTY)) {
                            boxes.add(currentCoords);
                        } // The field is different => the situations are not compatible.
                        else if (situation2.getField(currentCoords) != SokobanGamePlan.FIELD_BOX_ON_EMPTY) {
                            throw new IllegalArgumentException("The situation has different field : " + currentCoords + ".");
                        }
                        break;
                    case SokobanGamePlan.FIELD_BOX_ON_GOAL:
                        // The box on this field has been moved.
                        if ((situation2.getField(currentCoords) == SokobanGamePlan.FIELD_GOAL) ||
                                (situation2.getField(currentCoords) == SokobanGamePlan.FIELD_PERSON_ON_GOAL)) {
                            boxes.add(currentCoords);
                        } // The field is different => the situations are not compatible.
                        else if (situation2.getField(currentCoords) != SokobanGamePlan.FIELD_BOX_ON_GOAL) {
                            throw new IllegalArgumentException("The situations have different field : " + currentCoords + ".");
                        }
                        break;
                    case SokobanGamePlan.FIELD_WALL:
                        // The field with wall is different => the situations are not compatible.
                        if (situation2.getField(currentCoords) != SokobanGamePlan.FIELD_WALL) {
                            throw new IllegalArgumentException("The situations have different field : " + currentCoords + ".");
                        }
                        break;
                    case SokobanGamePlan.FIELD_EMPTY:
                    case SokobanGamePlan.FIELD_PERSON_ON_EMPTY:
                        // A box has been moved on this field.
                        if (situation2.getField(currentCoords) == SokobanGamePlan.FIELD_BOX_ON_EMPTY) {
                            targets.add(currentCoords);
                        } // The field is different => the situations are not compatible.
                        else if ((situation2.getField(currentCoords) != SokobanGamePlan.FIELD_EMPTY) &&
                                (situation2.getField(currentCoords) != SokobanGamePlan.FIELD_PERSON_ON_EMPTY)) {

                            throw new IllegalArgumentException("The situations have different field : " + currentCoords + ".");
                        }
                        break;
                    case SokobanGamePlan.FIELD_GOAL:
                    case SokobanGamePlan.FIELD_PERSON_ON_GOAL:
                        // A box has been moved on this field.
                        if (situation2.getField(currentCoords) == SokobanGamePlan.FIELD_BOX_ON_GOAL) {
                            targets.add(currentCoords);
                        } // The field is different => the situations are not compatible.
                        else if ((situation2.getField(currentCoords) != SokobanGamePlan.FIELD_GOAL) &&
                                (situation2.getField(currentCoords) != SokobanGamePlan.FIELD_PERSON_ON_GOAL)) {

                            throw new IllegalArgumentException("The situations have different field : " + currentCoords + ".");
                        }
                        break;
                }
            }
        }
        if (boxes.size() != targets.size()) {
            throw new IllegalArgumentException("The situations have different number of boxes.");
        }
        fixedBoxes.addAll(((SokobanGamePlan)situation1).getBoxes());
        fixedBoxes.removeAll(boxes);
    }

    @Override
    protected Collection<GamePlan> move(GamePlan plan) {
        if (plan == null) {
            throw new NullPointerException("Argument 'plan' is null.");
        }
        if (!(plan instanceof SokobanGamePlan)) {
            throw new IllegalArgumentException("Argument 'plan' is not instance of SokobanGamePlan.");
        }
        Set<GamePlan> result = new HashSet<GamePlan>();
        Path personPath = new PersonPath((SokobanGamePlan)plan);

        // For each box which is not fixed I try to push it to all directions.
        for (Coordinates box : ((SokobanGamePlan)plan).getBoxes()) {
            if (fixedBoxes.contains(box)) {
                continue;
            }
            Coordinates north = new Coordinates(box.getX(), box.getY() - 1);
            Coordinates south = new Coordinates(box.getX(), box.getY() + 1);
            Coordinates west = new Coordinates(box.getX() - 1, box.getY());
            Coordinates east = new Coordinates(box.getX() + 1, box.getY());

            // Pushing south.
            if (north.isInRange(0, 0, plan.columns() - 1, plan.rows() - 1) && personPath.get(north) != null) {
                SokobanGamePlan newPlan = new SokobanGamePlan(plan.getPlan());
                // If the person can go to the north field he can move to there
                // and try to push the box.
                newPlan.changePerson(north);
                if (newPlan.move(SokobanGamePlan.MOVE_SOUTH)) {
                    result.add(newPlan);
                }
            }

            // Pushing north.
            if (south.isInRange(0, 0, plan.columns() - 1, plan.rows() - 1) && personPath.get(south) != null) {
                SokobanGamePlan newPlan = new SokobanGamePlan(plan.getPlan());
                // If the person can go to the south field he can move to there
                // and try to push the box.
                newPlan.changePerson(south);
                if (newPlan.move(SokobanGamePlan.MOVE_NORTH)) {
                    result.add(newPlan);
                }
            }

            // Pushing west.
            if (east.isInRange(0, 0, plan.columns() - 1, plan.rows() - 1) && personPath.get(east) != null) {
                SokobanGamePlan newPlan = new SokobanGamePlan(plan.getPlan());
                // If the person can go to the east field he can move to there
                // and try to push the box.
                newPlan.changePerson(east);
                if (newPlan.move(SokobanGamePlan.MOVE_WEST)) {
                    result.add(newPlan);
                }
            }

            // Pushing east.
            if (west.isInRange(0, 0, plan.columns() - 1, plan.rows() - 1) && personPath.get(west) != null) {
                SokobanGamePlan newPlan = new SokobanGamePlan(plan.getPlan());
                // If the person can go to the south field he can move to there
                // and try to push the box.
                newPlan.changePerson(west);
                if (newPlan.move(SokobanGamePlan.MOVE_EAST)) {
                    result.add(newPlan);
                }
            }
        }
        return result;
    }
}
